Add evdev hotkey profile modifiers for per-recording post-processing#293
Add evdev hotkey profile modifiers for per-recording post-processing#293materemias wants to merge 3 commits intomainfrom
Conversation
Allow modifier keys to activate named profiles when held during hotkey press. This enables different post-processing pipelines (e.g., translate to English) without separate keybindings or compositor support. New config option [hotkey.profile_modifiers] maps evdev key names to profile names. The profile override file mechanism is reused from the existing CLI --profile flag, so all existing profile features (custom post-process command, timeout, output mode) work automatically. Includes cleanup on error/shutdown paths and validation warning when profile modifier keys overlap with required modifiers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add profile_modifiers to configuration guide, user manual, and default config file with examples and usage notes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds support for selecting a post-processing profile based on a held modifier key when using evdev hotkey detection, by propagating a profile_override through the hotkey event and reusing the existing profile_override runtime-file mechanism used by the CLI --profile flag.
Changes:
- Add
[hotkey.profile_modifiers]config to map modifier keys to profile names. - Emit
HotkeyEvent::Pressed { profile_override }from the evdev listener based on held profile-modifier state. - Have the daemon write/cleanup the
profile_overrideruntime file based on hotkey events; update docs and defaults with examples.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/config.rs |
Adds HotkeyConfig.profile_modifiers plus parsing tests. |
src/hotkey/mod.rs |
Extends HotkeyEvent::Pressed to include profile_override. |
src/hotkey/evdev_listener.rs |
Parses profile modifiers, tracks held state, and emits profile override on hotkey press. |
src/daemon.rs |
Writes/cleans profile_override file based on hotkey events and shutdown/error paths. |
docs/CONFIGURATION.md |
Documents new [hotkey.profile_modifiers] section with examples. |
docs/USER_MANUAL.md |
Adds a “Profile Modifiers” subsection under hotkeys with example config. |
config/default.toml |
Adds commented examples for model_modifier and hotkey.profile_modifiers. |
| // Track profile modifier state | ||
| if let Some(profile_name) = profile_modifiers.get(&key) { | ||
| match value { | ||
| 1 => active_profile_modifier = Some(profile_name.clone()), | ||
| 0 => { | ||
| if active_profile_modifier.as_deref() == Some(profile_name.as_str()) { | ||
| active_profile_modifier = None; | ||
| } | ||
| } | ||
| _ => {} | ||
| } | ||
| } |
There was a problem hiding this comment.
active_profile_modifier only tracks a single profile name, which breaks when multiple profile modifier keys are held (e.g., press A then B, release B => profile clears even if A is still held). Consider tracking held profile-modifier keys (e.g., a HashSet<Key> or counts) and deriving the active profile deterministically at hotkey-press time (including a defined precedence when multiple are held).
There was a problem hiding this comment.
Fixed in b811b0b. Replaced the single Option<String> with HashMap<Key, String> that tracks all held profile modifier keys independently. Releasing one key only removes that key's entry, so other held modifiers are unaffected.
src/hotkey/evdev_listener.rs
Outdated
| // Track which profile modifier is currently held | ||
| let mut active_profile_modifier: Option<String> = None; | ||
|
|
There was a problem hiding this comment.
active_profile_modifier is introduced as additional listener state, but it is not cleared in the same places where other state is cleared (device change handling and stale-device validation). This can leave a “sticky” profile override after devices are re-enumerated/removed. Reset this state alongside active_modifiers, model_modifier_held, and is_pressed.
There was a problem hiding this comment.
Fixed in b811b0b. Added held_profile_modifiers.clear() in both device-change paths (inotify detection and periodic stale-device validation), alongside the existing active_modifiers/model_modifier_held/is_pressed resets.
| [profiles.translate] | ||
| post_process_command = "my-script.sh --translate-en" | ||
| timeout_ms = 10000 | ||
|
|
||
| [profiles.formal] | ||
| post_process_command = "my-script.sh --formal" | ||
| ``` |
There was a problem hiding this comment.
The example sets timeout_ms inside [profiles.translate], but the profile field is post_process_timeout_ms (while timeout_ms belongs under [output.post_process]). As written, this timeout won’t apply to the profile and will likely be ignored by config parsing. Update the example (and surrounding text if needed) to use post_process_timeout_ms for profiles.
There was a problem hiding this comment.
Fixed in b811b0b. Changed to post_process_timeout_ms.
docs/USER_MANUAL.md
Outdated
|
|
||
| [profiles.translate] | ||
| post_process_command = "my-cleanup.sh --translate-en" | ||
| timeout_ms = 10000 |
There was a problem hiding this comment.
The example uses timeout_ms under [profiles.translate], but profile timeouts are configured via post_process_timeout_ms (whereas timeout_ms is for [output.post_process]). This example currently won’t set the intended profile-specific timeout.
| timeout_ms = 10000 | |
| post_process_timeout_ms = 10000 |
There was a problem hiding this comment.
Fixed in b811b0b. Changed to post_process_timeout_ms.
src/daemon.rs
Outdated
| tracing::info!("Starting voxtype daemon"); | ||
|
|
||
| // Clean up any stale cancel file from previous runs | ||
| // Clean up any stale override/cancel files from previous runs |
There was a problem hiding this comment.
The startup comment says “stale override/cancel files,” but this block only cleans cancel and profile_override (not other override files like output_mode_override, model_override, or *_override). Either narrow the comment to match what’s actually cleaned up, or extend the cleanup to cover all override files that should not persist across daemon restarts.
| // Clean up any stale override/cancel files from previous runs | |
| // Clean up any stale cancel and profile override files from previous runs |
There was a problem hiding this comment.
Fixed in b811b0b. Narrowed the comment to match what's actually cleaned up.
Replace single-value active_profile_modifier with a HashMap of held keys so releasing one modifier doesn't clear another's profile. Reset held_profile_modifiers on device re-enumeration and stale device removal. Fix doc examples using wrong field name (timeout_ms -> post_process_timeout_ms). Narrow daemon startup cleanup comment to match actual behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
[hotkey.profile_modifiers]config for evdev hotkey detection: map modifier keys to named profiles so holding a modifier while pressing the hotkey activates a different post-processing pipeline--profileflag), so all profile features (custom post-process command, timeout, output mode) work automaticallyExample config:
Bare ScrollLock uses default post-processing; Right Shift + ScrollLock translates to English.
Only applies to evdev hotkey detection (
enabled = true). When using compositor keybindings, usevoxtype record start --profile <name>instead.Files changed
src/config.rs— Newprofile_modifiersfield onHotkeyConfig+ 2 testssrc/hotkey/mod.rs—profile_overridefield onHotkeyEvent::Pressedsrc/hotkey/evdev_listener.rs— Parse, track, and emit profile modifier statesrc/daemon.rs— Write profile override file from hotkey event; cleanup on error/shutdown/startupdocs/CONFIGURATION.md— New[hotkey.profile_modifiers]sectiondocs/USER_MANUAL.md— Profile modifiers subsection under hotkeysconfig/default.toml— Commented exampleTest plan
cargo test— 549 tests pass (including 2 new profile_modifiers config tests)cargo clippy— no new warnings🤖 Generated with Claude Code